#include "skia_framework.h"

#include <chrono>
#include <cstring>
#include <functional>
#include <iomanip>
#include <iostream>
#include <sstream>

#include <GL/glew.h>
#include <GLFW/glfw3.h>

#include <include/core/SkBitmap.h>
#include <include/core/SkCanvas.h>
#include <include/core/SkFontMetrics.h>
#include <include/core/SkFontMgr.h>
#include <include/core/SkPath.h>
#include <include/core/SkSurface.h>
#include <include/core/SkTextBlob.h>
#include <include/gpu/gl/GrGLInterface.h>
#include <include/gpu/GrDirectContext.h>

#include "iskia_control.h"

using namespace std::chrono_literals;

struct GLData {
    bool glfwInited = false;
    GLFWwindow* glfwWindow = nullptr;
    sk_sp<GrDirectContext> glctx = nullptr;
    bool needResize = true;
    sk_sp<SkSurface> skSurface = nullptr;
};

SkiaFramework::SkiaFramework()
{
    data_ = new struct GLData();
}

SkiaFramework::~SkiaFramework()
{
    auto &gldata = *reinterpret_cast<struct GLData *>(data_);
    if (uiThread_) {
        uiThread_->join();
    }

    if (gldata.glfwWindow != nullptr) {
        glfwDestroyWindow(gldata.glfwWindow);
    }

    if (gldata.glfwInited == true) {
        glfwTerminate();
    }
}

void SkiaFramework::SetDrawFunc(const DrawFunc &onDraw)
{
    onDraw_ = onDraw;
}

void SkiaFramework::SetDraw2Func(const Draw2Func &onDraw)
{
    onDraw2_ = onDraw;
}

void SkiaFramework::SetResizeFunc(const ResizeFunc &onResize)
{
    onResize_ = onResize;
}

void SkiaFramework::SetGPUAllowance(bool allow)
{
    allowGPU_ = allow;
}

void SkiaFramework::SetWindowWidth(int width)
{
    windowWidth_ = width;
}

void SkiaFramework::SetWindowHeight(int height)
{
    windowHeight_ = height;
}

void SkiaFramework::SetWindowTitle(const std::string &title)
{
    windowTitle_ = title;
}

int SkiaFramework::GetWindowWidth() const
{
    return windowWidth_;
}

int SkiaFramework::GetWindowHeight() const
{
    return windowHeight_;
}

void SkiaFramework::Run()
{
    auto &gldata = *reinterpret_cast<struct GLData *>(data_);
    if (!glfwInit()) {
        std::cerr << "glfwInit failed" << std::endl;
        return;
    }
    gldata.glfwInited = true;

    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    gldata.glfwWindow = glfwCreateWindow(windowWidth_, windowHeight_, "glfw shadow window", nullptr, nullptr);
    if (gldata.glfwWindow == nullptr) {
        std::cerr << "glfwCreateWindow failed" << std::endl;
        std::cerr << "errno: " << errno << ", means " << strerror(errno) << std::endl;
        return;
    }
    glfwSetWindowUserPointer(gldata.glfwWindow, this);

    static auto That = [](GLFWwindow *window) -> SkiaFramework & {
        return *reinterpret_cast<SkiaFramework *>(glfwGetWindowUserPointer(window));
    };

    glfwSetWindowCloseCallback(gldata.glfwWindow, [](auto window) {
        auto &that = That(window);
        std::lock_guard<std::mutex> lock(that.propsMutex_);
        that.uiThreadRunning_ = false;
    });

    glfwSetWindowSizeCallback(gldata.glfwWindow, [](auto window, int w, int h) {
        auto &that = That(window);
        std::lock_guard<std::mutex> lock(that.propsMutex_);
        auto &gldata = *reinterpret_cast<struct GLData *>(that.data_);
        gldata.needResize = true;
        if (that.onResize_) {
            that.onResize_(w, h);
        }
        that.windowWidth_ = w;
        that.windowHeight_ = h;
    });

    glfwSetMouseButtonCallback(gldata.glfwWindow, [](auto window, int button, int action, int mods) {
        auto &that = That(window);
        std::lock_guard<std::mutex> lock(that.propsMutex_);
        if (button == GLFW_MOUSE_BUTTON_LEFT && action == GLFW_RELEASE) {
            that.left_ = false;
            if (that.click_) {
                auto point = that.invmat_.mapXY(that.clickx_, that.clicky_);
                for (const auto &control : that.controls) {
                    if (control->IsContain(point.x(), point.y()) == false) {
                        continue;
                    }

                    control->OnClick(point.x(), point.y());
                }
            }

            for (const auto &control : that.controls) {
                control->OnMouseUp();
            }
        } else if (button == GLFW_MOUSE_BUTTON_LEFT && action == GLFW_PRESS) {
            that.left_ = true;
            that.click_ = true;
            double x, y;
            glfwGetCursorPos(window, &x, &y);
            that.clickx_ = x;
            that.clicky_ = y;

            for (const auto &control : that.controls) {
                auto point = that.invmat_.mapXY(that.clickx_, that.clicky_);
                control->OnMouseDown(point.x(), point.y());
            }
        } else if (button == GLFW_MOUSE_BUTTON_RIGHT && action == GLFW_RELEASE) {
            double x, y;
            glfwGetCursorPos(window, &x, &y);
            that.right_ = false;
            that.mat_ = SkMatrix::Translate(-that.diffx_, -that.diffy_) * that.mat_;
            that.mat_ = SkMatrix::Translate(x - that.downRX_, y - that.downRY_) * that.mat_;
            that.UpdateInvertMatrix();
            that.diffx_ = that.diffy_ = 0;
        } else if (button == GLFW_MOUSE_BUTTON_RIGHT && action == GLFW_PRESS) {
            double x, y;
            glfwGetCursorPos(window, &x, &y);
            that.right_ = true;
            that.downRX_ = x;
            that.downRY_ = y;
        }
    });

    glfwSetCursorPosCallback(gldata.glfwWindow, [](auto window, double x, double y) {
        auto &that = That(window);
        std::lock_guard<std::mutex> lock(that.propsMutex_);
        that.x_ = x;
        that.y_ = y;

        if (that.right_) {
            that.mat_ = SkMatrix::Translate(-that.diffx_, -that.diffy_) * that.mat_;
            that.diffx_ = that.x_ - that.downRX_;
            that.diffy_ = that.y_ - that.downRY_;
            that.mat_ = SkMatrix::Translate(that.diffx_, that.diffy_) * that.mat_;
        }
        that.UpdateInvertMatrix();

        that.click_ = false;
        auto point = that.invmat_.mapXY(that.x_, that.y_);
        for (const auto &control : that.controls) {
            control->OnMouseMotion(point.x(), point.y());
        }
    });

    glfwSetScrollCallback(gldata.glfwWindow, [](auto window, double x, double y) {
        auto &that = That(window);
        std::lock_guard<std::mutex> lock(that.propsMutex_);
        auto point = that.invmat_.mapXY(that.x_, that.y_);
        that.mat_ = that.mat_ * SkMatrix::Translate(+point.x(), +point.y());
        if (y > 0) {
            that.mat_ = that.mat_ * SkMatrix::Scale(1.2, 1.2);
        } else {
            that.mat_ = that.mat_ * SkMatrix::Scale(1 / 1.2, 1 / 1.2);
        }
        that.mat_ = that.mat_ * SkMatrix::Translate(-point.x(), -point.y());
        that.UpdateInvertMatrix();
    });

    glfwSetKeyCallback(gldata.glfwWindow, [](auto window, int key, int scancode, int action, int mods) {
        if (key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE) {
            exit(0);
        }
    });

    uiThreadRunning_ = true;
    uiThread_ = std::make_unique<std::thread>(std::bind(&SkiaFramework::UIThreadMain, this));

    while (uiThreadRunning_) {
        glfwWaitEvents();
    }
}

void SkiaFramework::UpdateInvertMatrix()
{
    if (auto ret = mat_.invert(&invmat_); ret == false) {
        invmat_ = SkMatrix::I();
    }
}

void SkiaFramework::DrawString(SkCanvas &canvas, const std::string &str, double x, double y)
{
    SkPaint textPaint;
    textPaint.setAntiAlias(true);
    textPaint.setColor(0xff0066ff);
    textPaint.setStyle(SkPaint::kFill_Style);

    SkFont font;
    font.setTypeface(SkTypeface::MakeFromFile("out/DroidSansFallback.ttf"));
    font.setSize(16);
    canvas.drawString(str.c_str(), x, y, font, textPaint);
}

SkPoint3 SkiaFramework::MeasureString(const std::string &str)
{
    SkFont font;
    font.setTypeface(SkTypeface::MakeFromFile("out/DroidSansFallback.ttf"));
    font.setSize(16);
    auto width = font.measureText(str.data(), str.length(), SkTextEncoding::kUTF8);
    SkFontMetrics metrics;
    font.getMetrics(&metrics);
    return {width, -metrics.fAscent + metrics.fDescent, -metrics.fAscent};
}

void SkiaFramework::AddControl(const std::shared_ptr<ISkiaControl> &control)
{
    controls.push_back(control);
}

void SkiaFramework::UIThreadMain()
{
    auto &gldata = *reinterpret_cast<struct GLData *>(data_);
    glfwMakeContextCurrent(gldata.glfwWindow);
    sk_sp<const GrGLInterface> glinterface{GrGLCreateNativeInterface()};
    if (glinterface == nullptr) {
        std::cerr << "glinterface == nullptr" << std::endl;
        return;
    }

    gldata.glctx = GrDirectContext::MakeGL(glinterface);
    if (gldata.glctx == nullptr) {
        std::cerr << "grContext == nullptr" << std::endl;
        return;
    }

    while (uiThreadRunning_) {
        std::this_thread::sleep_for(16ms);

        std::lock_guard<std::mutex> lock(propsMutex_);
        glfwMakeContextCurrent(gldata.glfwWindow);

        if (gldata.needResize) {
            gldata.needResize = false;
            if (gldata.glctx) {
                SkColorType colorType = kRGBA_8888_SkColorType;
                sk_sp<SkColorSpace> skColorSpace = SkColorSpace::MakeSRGB();
                SkSurfaceProps surfaceProps;
                GrBackendRenderTarget backendRenderTarget(windowWidth_,
                    windowHeight_, 0, 8, {.fFBOID = 0, .fFormat = GL_RGBA8});
                gldata.skSurface = SkSurface::MakeFromBackendRenderTarget(gldata.glctx.get(),
                    backendRenderTarget, kBottomLeft_GrSurfaceOrigin, colorType, skColorSpace, &surfaceProps);
            }
        }

        SkCanvas &canvas = *gldata.skSurface->getCanvas();
        canvas.resetMatrix();
        canvas.save();

        canvas.clear(SK_ColorWHITE);
        canvas.setMatrix(mat_);
        DrawBefore(canvas);
        if (onDraw_) {
            onDraw_(canvas);
        }

        if (onDraw2_) {
            onDraw2_(canvas, *gldata.skSurface);
        }

        for (const auto &control : controls) {
            control->OnDraw(canvas);
        }

        canvas.restore();
        // DrawColorPicker(canvas, bitmap);
        DrawAfter(canvas);

        gldata.skSurface->flushAndSubmit(true);
        glfwSwapBuffers(gldata.glfwWindow);
    }
}

void SkiaFramework::DrawBefore(SkCanvas &canvas)
{
    SkPaint paint;
    paint.setAntiAlias(true);
    paint.setColor(0x09000000);
    paint.setStyle(SkPaint::kStroke_Style);
    paint.setStrokeWidth(1);
    SkPaint paint2 = paint;
    paint2.setColor(0x22000000);

    SkPaint textPaint;
    textPaint.setAntiAlias(true);
    textPaint.setColor(0xff00007f);

    SkFont font;
    font.setTypeface(SkTypeface::MakeFromFile("out/DroidSansFallback.ttf"));
    font.setSize(16);

    auto rect = invmat_.mapRect(SkRect::MakeXYWH(0, 0, windowWidth_, windowHeight_));
    auto left = static_cast<int>(rect.left()) / 100 * 100;
    auto right = static_cast<int>(rect.right());
    auto top = static_cast<int>(rect.top()) / 100 * 100;
    auto bottom = static_cast<int>(rect.bottom());

    SkPath path;
    for (int i = left; i <= right; i += 20) {
        path.moveTo(i, -1e9);
        path.lineTo(i, 1e9);
    }

    for (int i = top; i <= bottom; i += 20) {
        path.moveTo(-1e9, i);
        path.lineTo(1e9, i);
    }
    canvas.drawPath(path, paint);

    SkPath path2;
    for (int i = left; i <= right; i += 100) {
        path2.moveTo(i, -1e9);
        path2.lineTo(i, 1e9);

        std::stringstream ss;
        ss << i;
        canvas.drawString(ss.str().c_str(),
                          i,
                          font.getSize() + 0,
                          font, textPaint);
    }

    for (int i = top; i <= bottom; i += 100) {
        path2.moveTo(-1e9, i);
        path2.lineTo(1e9, i);

        std::stringstream ss;
        ss << i;
        canvas.drawString(ss.str().c_str(),
                          0,
                          font.getSize() + i,
                          font, textPaint);
    }
    canvas.drawPath(path2, paint2);
}

void SkiaFramework::DrawColorPicker(SkCanvas &canvas, SkBitmap &bitmap)
{
    if (left_) {
        SkFont font;
        font.setTypeface(SkTypeface::MakeFromFile("out/DroidSansFallback.ttf"));
        font.setSize(24);

        SkPaint paint;
        paint.setAntiAlias(true);
        paint.setStyle(SkPaint::kFill_Style);
        SkPaint paint2;
        paint2.setAntiAlias(true);
        paint2.setColor(SK_ColorBLACK);
        paint2.setStyle(paint2.kStroke_Style);

        auto color = bitmap.getColor(x_, y_);
        paint.setColor(color);
        std::stringstream ss;
        ss << std::hex << std::setfill('0') << std::setw(6) << color;
        canvas.drawString(ss.str().c_str(), x_, y_, font, paint2);
        canvas.drawString(ss.str().c_str(), x_, y_, font, paint);
    }
}

void SkiaFramework::DrawAfter(SkCanvas &canvas)
{
    SkPaint paint;
    paint.setAntiAlias(true);
    paint.setColor(0x33000000);
    paint.setStyle(SkPaint::kStroke_Style);
    paint.setStrokeWidth(1);

    SkPaint textPaint;
    textPaint.setAntiAlias(true);
    textPaint.setColor(0xff0000ff);
    textPaint.setStyle(SkPaint::kStroke_Style);
    textPaint.setStrokeWidth(1);

    SkFont font;
    font.setTypeface(SkTypeface::MakeFromFile("out/DroidSansFallback.ttf"));
    font.setSize(16);

    SkPath path;
    if (left_) {
        path.moveTo(x_, 0);
        path.lineTo(x_, 1e9);
        path.moveTo(0, y_);
        path.lineTo(1e9, y_);

        auto point = invmat_.mapXY(x_, y_);
        std::stringstream ss;
        ss << "(" << point.x() << ", " << point.y() << ")";
        canvas.drawString(ss.str().c_str(),
                          x_ + 10,
                          font.getSize() + y_ + 10,
                          font, textPaint);
    }

    canvas.drawPath(path, paint);
}
